// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';
import 'dart:typed_data';
import 'package:compiler/compiler_api.dart' as api;
import 'package:compiler/src/commandline_options.dart';
import 'package:compiler/src/util/memory_compiler.dart';
import 'package:expect/expect.dart';

const int numShards = 4;
const List<String> dataDirs = ['data', '../inference/data'];
const Set<String> needsLibs = {'deferred.dart'};

const jsOutFilename = 'out.js';
const sourceMapFilename = '${jsOutFilename}.map';
const dumpInfoFilename = 'out.info.json';

typedef CompiledOutput = Map<api.OutputType, Map<String, String>>;

Future<CompiledOutput> compileWithSerialization({
  Uri? entryPoint,
  required Map<String, dynamic> memorySourceFiles,
  required List<String> options,
}) async {
  final cfeDillUri = 'memory:cfe.dill';
  final closedWorldUri = 'memory:world.data';
  final globalDataUri = 'memory:global.data';
  final codegenUri = 'memory:codegen';
  final jsOutUri = 'memory:$jsOutFilename';
  Future<CompiledOutput> compile(List<String> options) async {
    final outputProvider = OutputCollector();
    CompilationResult result = await runCompiler(
      entryPoint: entryPoint,
      memorySourceFiles: memorySourceFiles,
      outputProvider: outputProvider,
      options: options,
    );
    Expect.isTrue(result.isSuccess);
    outputProvider.binaryOutputMap.forEach((fileName, binarySink) {
      memorySourceFiles[fileName.path] = Uint8List.fromList(binarySink.list);
    });
    return outputProvider.clear();
  }

  final commonFlags = [
    '${Flags.closedWorldUri}=$closedWorldUri',
    '${Flags.globalInferenceUri}=$globalDataUri',
    '${Flags.codegenUri}=$codegenUri',
    '${Flags.codegenShards}=2',
  ];

  await compile(
    [...options, '--out=$cfeDillUri', '${Flags.stage}=cfe'] + commonFlags,
  );
  await compile(
    [
          ...options,
          '${Flags.inputDill}=$cfeDillUri',
          '${Flags.stage}=closed-world',
        ] +
        commonFlags,
  );
  await compile(
    [
          ...options,
          '${Flags.inputDill}=$cfeDillUri',
          '${Flags.stage}=global-inference',
        ] +
        commonFlags,
  );
  await compile(
    [
          ...options,
          '${Flags.inputDill}=$cfeDillUri',
          '${Flags.stage}=codegen',
          '${Flags.codegenShard}=0',
        ] +
        commonFlags,
  );
  await compile(
    [
          ...options,
          '${Flags.inputDill}=$cfeDillUri',
          '${Flags.stage}=codegen',
          '${Flags.codegenShard}=1',
        ] +
        commonFlags,
  );
  final output = await compile(
    [
          ...options,
          '${Flags.inputDill}=$cfeDillUri',
          '--out=$jsOutUri',
          '${Flags.stage}=emit-js',
        ] +
        commonFlags,
  );
  return output;
}

Future<CompiledOutput> compileWithoutSerialization({
  Uri? entryPoint,
  required Map<String, dynamic> memorySourceFiles,
  required List<String> options,
}) async {
  final jsOutUri = 'memory:$jsOutFilename';
  final outputProvider = OutputCollector();

  CompilationResult result = await runCompiler(
    entryPoint: entryPoint,
    memorySourceFiles: memorySourceFiles,
    outputProvider: outputProvider,
    options: [...options, '--out=$jsOutUri'],
  );
  Expect.isTrue(result.isSuccess);

  return outputProvider.clear();
}

// TODO(natebiggs): Check dump info diff when the results are aligned with and
//   without serialization.
Set<api.OutputType> _outputTypesToIgnore = {api.OutputType.dumpInfo};

Future<void> compareResults(
  CompiledOutput serializationResult,
  CompiledOutput noSerializationResult,
) async {
  serializationResult.forEach((outputType, outputs) {
    if (_outputTypesToIgnore.contains(outputType)) return;
    Expect.isTrue(noSerializationResult.containsKey(outputType));
    final otherOutputs = noSerializationResult[outputType]!;
    Expect.mapEquals(outputs, otherOutputs);
  });
}

Future runTests(List<String> args, int shard) async {
  assert(
    shard >= 0 && shard < numShards,
    'Shard must be between 0 and ${numShards - 1} (inclusive)',
  );

  final libDirectory = Directory.fromUri(Platform.script.resolve('libs'));

  final testFiles = dataDirs
      .map((e) => Directory.fromUri(Platform.script.resolve('data')))
      .expand((dataDir) => dataDir.listSync());
  int i = 0;
  for (final testFile in testFiles) {
    if (!testFile.uri.pathSegments.last.endsWith('.dart')) continue;
    if (i++ % numShards != shard) continue;

    String name = testFile.uri.pathSegments.last;
    List<String> testOptions = [Flags.dumpInfo];
    print('----------------------------------------------------------------');
    print('Test file: ${testFile.uri}');
    String mainCode = await File.fromUri(testFile.uri).readAsString();
    final mainUri = _createUri('src/main.dart');
    Map<String, dynamic> memorySourceFiles = {mainUri.path: mainCode};

    if (needsLibs.contains(name)) {
      print('Supporting libraries:');
      String filePrefix = name.substring(0, name.lastIndexOf('.'));
      await for (final libFile in libDirectory.list()) {
        String libFileName = libFile.uri.pathSegments.last;
        if (libFileName.startsWith(filePrefix)) {
          print('    - libs/$libFileName');
          Uri libFileUri = _createUri('libs/$libFileName');
          String libCode = await File.fromUri(libFile.uri).readAsString();
          memorySourceFiles[libFileUri.path] = libCode;
        }
      }
    }

    final serializationResult = await compileWithSerialization(
      entryPoint: mainUri,
      memorySourceFiles: memorySourceFiles,
      options: testOptions,
    );
    final noSerializationResult = await compileWithoutSerialization(
      entryPoint: mainUri,
      memorySourceFiles: memorySourceFiles,
      options: testOptions,
    );

    await compareResults(serializationResult, noSerializationResult);
  }
}

// Pretend this is a dart2js_native test to allow use of 'native' keyword
// and import of private libraries.
Uri _createUri(String fileName) {
  return Uri.parse('memory:sdk/tests/web/native/$fileName');
}
